…人被視為一個歷程,一個成為 (becoming) 的歷程。該模式相信,每一個人都可能改變。即使外在的改變很有限,內在的改變卻是可能的。這個信念是普世皆然、毫無限制的,它結合了終生皆有可塑性的觀點。……成為是流動性的,隨時都在改變,從一連串的抉擇當中開展。我們總是在成為什麼的過程中……
-- 約翰·貝曼, 薩提爾成長模式的應用
-- 0902
真的忘記為了解開那個題目,來回這裡多少次了。
不過解開的那一瞬間真是愉快啊。我還記得在整棟樓房亮起的那個時候,外面似乎有個低鳴的聲音。擔心又來一次城市崩毀,我衝出去探探究竟。
卻是城牆前有一整區的建築變得鮮明起來。其中最前方那一整面,更是遮蔽了原先可以一眼望過去的視野,城裡再也不似之前空若無物的樣子了。
一如以往,那塊泛紫金屬板又停在那邊了。我摸了一下,想了想,還是問了:「你有看過一隻棕紅色的小動物嗎?」
板子的表面上浮現了這個字樣:
Nothing <> Just "a book"
向上看,這一棟建築上的符號是:<$>
。我記得的,這棟跟之前若有似無的樣子真是天壤之別。
Functor 的實作
class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b (<$) :: a -> f b -> f a {-# MINIMAL fmap #-}
Functor 的法則:
- 封閉律
- 單位元素 (Identity)
- 分配律
Functor,跟它的中文翻譯:函子,雖然看起來一付很深奧的樣子,但其實概念上非常簡單。可以說寫程式一陣子的人裡,應該很難找到沒用過的。
每當你使用了串列/陣列的 map
,你就是在使用它的 Functor 特質所帶來的好處,而我們在之前已經看過好幾次了。
若是以串列/陣列當做容器,那麼 fmap
跟大家習慣的 map
是一模一樣的。另外 fmap
還有一個中綴版的符號:<$>
:
-- Haskell 語法
map (+1) [1, 2, 3] -- => [2, 3, 4] -- 串列就是串列
fmap (+1) [1, 2, 3] -- => [2, 3, 4] -- 把串列當 functor
-- 中綴版
(+1) <$> [1, 2, 3] -- => [2, 3, 4]
然而 functor 的優秀之處,並不僅限於串列。或者應該這麼說,當我們可以將 map
的概念,用於其它容器上時,談 Functor 才比較有意義。
若能把這個「改變容器內容,而維持外殼不變」的特質也抽象出來,那麼就會得出一個堪稱作弊模式的觀點:升格 (Lifting)。函子最有威力的視角,不在於改變容器的內容,而是把函式升格這件事,以及取得這個抽象後的運用手法。
$
為了要解釋什麼是升格,我們再來說明一個看起來沒什麼功能的函式:
($) :: (a -> b) -> a -> b
infixr 0 $
(+ 2) $ 10 -- -> 12
嗯… 這操作起來跟單純的函式呼叫一模一樣,那明明就用空白就可以啦?不過當我們試試看下面的寫法時,就會出現問題。
(+1) . (+2) 10
-- 出錯了!
-- => <interactive>:90:1: error:
-- • Non type-variable argument in the constraint: Num (a -> c)
-- (Use FlexibleContexts to permit this)
-- • When checking the inferred type
-- it :: forall c a. (Num c, Num (a -> c)) => a -> c
要問為什麼會這樣,原因是用空白來調用函式的優先順序極高,所以在上述的程式碼中,會先用 10
當引數來調用 (+ 2)
函式得到 12
。接著拿著這個數字試著與 (+1)
用 .
進行函式組合時就會出錯了。這個時候,我們可以選擇用括號把前面括起來:
-- Haskell 語法
((+1) . (+2)) 10 -- => 13
再看一下剛剛 $
的定義,仔細看的話,會發現它的優先度是…0
,最低優先。所以我們可以使用 $
來讓函式應用的順序變成最後才應用,來減少括號的干擾:
-- Haskell 語法
(+1) . (+2) $ 10 -- => 13
讓我們來比較一下 $
,也就是一般的函式應用 (apply) 與 fmap
,或說 <$>
的型別定義:
-- Haskell 語法
$ :: (a -> b) -> a -> b
<$> :: (a -> b) -> f a -> f b
仔細看一下,就會發現差別在當我們把 (a -> b)
的函式傳給 $
後,我們拿到的就是一個原封不動的 a -> b
函式。相對於此 ,<$>
的第二個參數跟回傳值,都是包在 f
裡面的。這個 f
代表的是具有 Functor 特質的容器,你可以暫時把這個 f
想像成串列的外殼。
而把 Haskell 惰性求值一起放進來考慮時,我們可以這樣說:如果我們把一個 (a -> b)
的函式當做參數,傳給 fmap
後,我們得到了一個升級版的函式:f a -> f b
。這個升級版的函式不是直接處理 a
型別,回傳 b
型別。而是能夠穿透容器的外殼,處理裝在 f
容器裡的 a
型別,並回傳裝在 f
容器裡的 b
型別。維持其外殼 f
不變。
所以 Haskell 裡是這麼說的:fmap
可以讓一個函式升格成變動容器內容的函式。
id
而函式的單位元素,就是我們之前提過的恆等函式:id
。而用法則是這樣的:
-- Haskell 語法
fmap id = id
-- 試試看。對某個 functor 用 id 去 fmap,等同於把該 functor 直接傳入 id 函式。
fmap id "Hi functor" -- => "Hi functor"
id "Hi functor" -- => "Hi functor"
再來看一下數學,例如下面這個式子,我們會說乘法對加法符合分配律:
那麼對於 Functor 來說,fmap
對函式組合也符合分配律:fmap (f . g)
等同於 fmap f . fmap g
當你用 fmap
來升格 f . g
這個函式組合,會等於於將 f
與 g
分別升格之後,再進行函式組合。
-- Haskell 語法
f = (+1)
g = (*3)
fmap (f . g) $ [1, 2, 3] -- => [4,7,10]
fmap f . fmap g $ [1, 2, 3] -- => [4,7,10]
我本來想隨便丟個串列進去門上那個孔的,但是當我手上拿著空的串列靠進門的時候,伴隨著門上的光線很不明顯的變暗了一下,我似乎聽到了一聲很輕的嘆息。我想,可能它吃這個吃得很膩了吧。我走遠了一點四處翻找,終於讓我找到一個 Nothing
。
[to be continue]